package de.recipeminer.environments;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.log4j.Logger;
import org.yaml.snakeyaml.Yaml;

import java.io.InputStream;
import java.io.Reader;
import java.util.Arrays;
import java.util.Deque;
import java.util.Map;

/**
 * @autor: Markus Ackermann
 */
public abstract class YamlConfigValidator {
    private static Logger logger = Logger.getLogger(YamlConfigValidator.class);
    private String errorPrefix = "YAML file with invalid structure:\n";
    
    abstract protected YamlStructureAssertion[] assertions();

    public Object validate(Reader reader) {
        return validate(new Yaml().load(reader));
    }


    public Object validate(InputStream input) {
        return validate(new Yaml().load(input));
    }

    public Object validate(Object yamlRoot) {
        if(assertions().length == 0)
            //no rule to validate
            return yamlRoot;
        Map rootMap = null;
        if(yamlRoot != null && yamlRoot instanceof Map)
            rootMap = (Map) yamlRoot;
        else {
            String msg = "YAML document should be a nested dictionary, but is apperenlty flat.";
            logger.error("YAML file with invalid structure:", new YamlPathAssertionException(msg));
            return null;
        }
        for(YamlStructureAssertion assertion : assertions()) {
            try {
                assertion.checkOn(rootMap);
            } catch (YamlPathAssertionException pathExcetion) {
                if( ! assertion.isOptionalElement()) {
                    logger.error(errorPrefix, pathExcetion);
                    return null;
                }
            }  catch (YamlTypeAssertionException typeException) {
                logger.error(errorPrefix, typeException);
                return null;
            }
        }
        return yamlRoot;
    }

    public void setErrorPrefix(String errorPrefix) {
        this.errorPrefix = errorPrefix;
    }

    private static class YamlStructureAssertion {
        private final static String PATH_SEP = "§";
        private final static String EXP_TYPE_SYMBOL = "instanceof";
        private boolean optional;
        private String path;
        private Class expectedType;

        public YamlStructureAssertion(String path, Class expectedType, boolean optional) {
            this.path = path;
            this.expectedType = expectedType;
            this.optional = optional;
        }

        public YamlStructureAssertion(String path, Class expectedType) {
            this.path = path;
            this.expectedType = expectedType;                     
            this.optional = false;
        }

        public boolean isOptionalElement() {
            return optional;
        }

        private String[] splitPath(){
            return path.split(PATH_SEP);
        }

        public void checkOn(Map yamlDoc) throws YamlTypeAssertionException, YamlPathAssertionException {
            //path to descend in the YAML-structure to find the YAML-element to be tested
            // (reduced from its head onwards)
            Deque<String> remainingPath = Lists.newLinkedList(Arrays.asList(splitPath()));

            //descend along given path
            Object pathEnd = descent(yamlDoc, remainingPath);
            //path descent ended successfully - check type of leaf
            if( ! expectedType.isInstance( pathEnd)) {
                String format = "%s%nLeaf scalar's type is %s instead of expected type %s";
                String actualType =  pathEnd.getClass().getSimpleName();
                String expectedType =  pathEnd.getClass().getSimpleName();
                throw new YamlTypeAssertionException(String.format(format,toString(),actualType, expectedType));
            }
        }
        
        private Object descent(Object node ,Deque<String> remainingPath) throws YamlPathAssertionException {
            if(remainingPath.size() == 0 & node != null)
                return node; // return existing end of path
            if(node == null || ! (node instanceof Map)) {
                String msg = String.format("%s%nUnable to reach leaf of path.", toString());
                throw new YamlPathAssertionException(msg);
            }
            node = ((Map) node).get(remainingPath.removeFirst());
            return descent(node, remainingPath);
        }
        
        @Override
        public String toString() {
            String template = "%s: %s %s %s";
            String className = getClass().getSimpleName();
            return String.format(template, className, path, EXP_TYPE_SYMBOL, expectedType.getSimpleName());
        }
    }

    public static class InvalidYamlStructureException extends Exception {
        public InvalidYamlStructureException(String msg) {
            super(msg);
        }
    }

    public static class YamlPathAssertionException extends Exception {
        public YamlPathAssertionException(String msg) {
            super(msg);
        }
    }

    public static class YamlTypeAssertionException extends Exception {
        public YamlTypeAssertionException(String msg) {
            super(msg);
        }
    }

    public static class EnvironmentDefinitionValidator extends YamlConfigValidator{
        @Override
        protected YamlStructureAssertion[] assertions() {
            return new YamlStructureAssertion[] {
                new YamlStructureAssertion("locations§configuration", String.class),
                new YamlStructureAssertion("locations§configuration_shared", String.class, true),
                new YamlStructureAssertion("locations§resource_files", String.class),
                new YamlStructureAssertion("locations§resource_files_shared", String.class, true),
                new YamlStructureAssertion("files§db4o_filename", String.class)
            };
        }
    }

    public static class EnvironmentFileValidator extends YamlConfigValidator{
        @Override
        protected YamlStructureAssertion[] assertions() {
            return  new YamlStructureAssertion[] {
                    new YamlStructureAssertion("default_environment", String.class),
                    new YamlStructureAssertion("environments", Map.class)
            };
        }

        Map<String, Map> checkAndGetEnvironments(InputStream stream) {
            return checkAndGetEnvironments(new Yaml().load(stream));
        }

        Map<String, Map> checkAndGetEnvironments(Reader reader) {
            return checkAndGetEnvironments(new Yaml().load(reader));
        }

        Map<String, Map> checkAndGetEnvironments(Object root) {
            Map rootAsMap = (Map) root;
            Map<String ,Map> validEnvironments = Maps.newHashMap();
            EnvironmentDefinitionValidator envDefValidator = new EnvironmentDefinitionValidator();            
            Map environments = (Map) rootAsMap.get("environments");
            for(Object envName : environments.keySet()) {
                String prefix = String.format("Invalid structure of environment definition of '%s'", envName);
                envDefValidator.setErrorPrefix(prefix);
                Map checkedDef = (Map) envDefValidator.validate(environments.get(envName));
                if(checkedDef != null) {
                    validEnvironments.put((String) envName, checkedDef);
                }
            }
            return validEnvironments;
        }
    }
}
